﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

namespace AwesomeEnginePC.Networking.Sockets
{
    public abstract class BaseSocketConnection<T> where T : BaseBrodcastMessage, new()
    {
        private object _lockObject;
        private List<SocketMessage> _messageQueue;

        public BaseSocketConnection()
        {
            MessageQueue = new List<SocketMessage>();
            _lockObject = new object();
        }

        protected List<SocketMessage> MessageQueue { get; set; }

        /// <summary>
        /// Gets the current items in the queue, and clears the queue.
        /// </summary>
        /// <returns></returns>
        public List<SocketMessage> GetQueueItems()
        {
            lock (_lockObject)
            {
                List<SocketMessage> messages = new List<SocketMessage>(MessageQueue);
                MessageQueue.Clear();
                return messages;
            }
        }

        protected void AddToQueue(SocketMessage msg)
        {
            MessageQueue.Add(msg);
        }

        protected void AddToQueue(SocketMessage.MessageType messageType, T state, bool wasSuccessful, int? clientSocketIndex)
        {
            AddToQueue(new SocketMessage(messageType, state, wasSuccessful, clientSocketIndex));
        }

        protected T ByteArrayToData(byte[] array)
        {
            MemoryStream memStream = new MemoryStream();
            BinaryFormatter binForm = new BinaryFormatter();
            memStream.Write(array, 0, array.Length);
            memStream.Seek(0, SeekOrigin.Begin);
            T data = (T)binForm.Deserialize(memStream);
            return data;
        }

        protected byte[] ObjectToByteArray(T data)
        {
            if (data == null)
                return null;

            BinaryFormatter bf = new BinaryFormatter();
            MemoryStream ms = new MemoryStream();
            bf.Serialize(ms, data);

            return ms.ToArray();
        }

        protected void SendData(T message, Socket socket)
        {
            byte[] data = ObjectToByteArray(message);
            byte[] length = BitConverter.GetBytes(data.Length);
            byte[] sendData = length.Concat(data).ToArray();

            socket.BeginSend(sendData, 0, sendData.Length, SocketFlags.None, new AsyncCallback(OnSend), message);
        }

        private void OnSend(IAsyncResult asyn)
        {
            AddToQueue(SocketMessage.MessageType.Send, new T(), asyn.IsCompleted, null);
        }

        protected void OnDataReceived(IAsyncResult asyn)
        {
            Packet packet = (Packet)asyn.AsyncState;

            try
            {
                packet.Socket.EndReceive(asyn);
            }
            catch (SocketException)
            {
                //the connection was terminated
                AddToQueue(SocketMessage.MessageType.Disconnection, new T(), false, GetClientSocketIndex(packet.Socket));
                return;
            }

            if (packet.DataBuffer.Length == Packet.DEFAULT_LENGTH)
            {
                //you have the header

                //convert bytes 1-5 for length of payload
                byte[] payloadLengthBytes = packet.DataBuffer
                    .Where((x, i) => i <= 3)
                    .Select(x => x)
                    .ToArray();

                int payloadLength = BitConverter.ToInt32(payloadLengthBytes, 0);

                WaitForData(packet.Socket, payloadLength);
            }
            else
            {
                //we have the payload
                T data = ByteArrayToData(packet.DataBuffer);
                AddToQueue(SocketMessage.MessageType.Receive, data, asyn.IsCompleted, GetClientSocketIndex(packet.Socket));

                WaitForData(packet.Socket);
            }
        }

        /// <summary>
        /// Gets the client socket index if this is the server. If this is the client, return null.
        /// </summary>
        /// <returns></returns>
        protected abstract int? GetClientSocketIndex(Socket socket);

        protected void WaitForData(Socket soc, int bufferLength = Packet.DEFAULT_LENGTH)
        {
            Packet packet = new Packet(bufferLength);
            packet.Socket = soc;

            soc.BeginReceive(packet.DataBuffer, 0, bufferLength, SocketFlags.None, new AsyncCallback(OnDataReceived), packet);
        }

        public class Packet
        {
            /// <summary>
            /// The default length of the message, which should be the length of the header.
            /// </summary>
            public const int DEFAULT_LENGTH = 4;

            public Packet()
            {
                DataBuffer = new byte[DEFAULT_LENGTH];
            }

            public Packet(int length)
            {
                DataBuffer = new byte[length];
            }

            public byte[] DataBuffer { get; set; }

            public Socket Socket { get; set; }
        }

        public class SocketMessage
        {
            public SocketMessage(MessageType messageType, T state, bool wasSuccessful, int? clientSocketIndex = null)
            {
                TypeOfMessage = messageType;
                State = state;
                WasSuccessful = wasSuccessful;
                ClientSocketIndex = clientSocketIndex;
            }

            public enum MessageType
            {
                Send,
                Receive,
                Disconnection,
                NewConnection,
                Init,
            }

            /// <summary>
            /// The state of the message, in context with the given MessageType.
            /// </summary>
            public T State { get; protected set; }

            public MessageType TypeOfMessage { get; protected set; }
            public bool WasSuccessful { get; protected set; }

            public int? ClientSocketIndex { get; protected set; }

            public override string ToString()
            {
                return string.Format("Type: {0}, Success: {1}, State: {2}, Client: {3}",
                    TypeOfMessage.ToString(), WasSuccessful.ToString(), State.ToStringOrValue(""), ClientSocketIndex.ToStringOrValue("_"));
            }
        }
    }

    public static class Extensions
    {
        public static string ToStringOrValue(this object source, string replacementValue)
        {
            if (source == null)
                return replacementValue;

            return source.ToString();
        }
    }
}
